﻿using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Globalization;
using System.Net;
using System.Threading;
using System.Xml.Linq;
using ThreeByte.ActiveDeck.Publish.Model;
#if SILVERLIGHT
using System.Windows;
#endif
using log4net;
using ThreeByte.ActiveDeck.Publish;
using ThreeByte.ActiveDeck.Service;
using System.Text.RegularExpressions;

//Based Heavily on http://laharsub.codeplex.com

namespace ThreeByte.ActiveDeck.Publisher.Client
{
    public class ActivePublisherClient
    {
        private static readonly ILog log = LogManager.GetLogger(typeof(ActivePublisherClient));
        private static readonly XNamespace ns = ServiceConfiguration.NAMESPACE;

        private NetworkCredential Credential;
        HttpLongPollManager pollManager;
        Dictionary<string, List<HttpLongPollManager>> pollManagers;
        object syncRoot = new object();

        bool MinimizeConnectionUse { get; set; }
        public Uri BaseAddress { get; private set; }


        public ActivePublisherClient(string baseAddress, NetworkCredential cred, bool minimizeConnectionUse = true)
        {
            Uri uri;
            if (!Uri.TryCreate(baseAddress, UriKind.Absolute, out uri))
            {
                throw new ArgumentException("The baseAddress parameter must be an absolute URI.");
            }
            else if (uri.AbsoluteUri.EndsWith("/"))
            {
                this.BaseAddress = uri;
            }
            else
            {
                this.BaseAddress = new Uri(uri.AbsoluteUri + "/");
            }
            Credential = cred;
            this.MinimizeConnectionUse = minimizeConnectionUse;
            if (this.MinimizeConnectionUse)
            {
                this.pollManager = new HttpLongPollManager(this.BaseAddress.AbsoluteUri, Credential);
            }
            else
            {
                this.pollManagers = new Dictionary<string, List<HttpLongPollManager>>();
            }
        }

        public void SubscribeAsync(Subscription subscription)
        {
            if (subscription == null)
            {
                throw new ArgumentNullException("subscription");
            }
            if (string.IsNullOrWhiteSpace(subscription.Channel))
            {
                throw new InvalidOperationException("Channel must be non-empty.");
            }
            if (subscription.From < 0)
            {
                throw new InvalidOperationException("From must be a non-negative integer.");
            }

            HttpLongPollManager pollManager = null;
            if (this.MinimizeConnectionUse)
            {
                pollManager = this.pollManager;
            }
            else
            {
                lock (this.syncRoot)
                {
                    if (!this.pollManagers.ContainsKey(subscription.Channel))
                    {
                        this.pollManagers[subscription.Channel] = new List<HttpLongPollManager>();
                    }
                    pollManager = new HttpLongPollManager(this.BaseAddress.AbsoluteUri, Credential);
                    this.pollManagers[subscription.Channel].Add(pollManager);
                }
            }

            pollManager.AddSubscription(subscription);
        }

        public void SubscribeAsync(string channel, Action<Subscription, ActiveMessage> onMessageReceived, Action<Subscription, Exception> onError)
        {
            this.SubscribeAsync(new Subscription { Channel = channel, OnMessageReceived = onMessageReceived, OnError = onError });
        }


        public void PublishAsync(ActiveMessage message)
        {
            this.PublishAsync(message, null, null);
        }

        public void PublishAsync(ActiveMessage message, Action<ActiveMessage> onSuccess, Action<ActiveMessage, Exception> onError)
        {
            if (message == null)
            {
                throw new ArgumentException("message");
            }
            if(string.IsNullOrWhiteSpace(message.Channel))
            {
                throw new InvalidOperationException("Channel must be non-empty");
            }
            if (message.Body == null)
            {
                throw new InvalidOperationException("Body of the message to publish must be specified.");
            }

            //Determine presentation id for URI
            string presPattern = @"(\d{6})_.+";

            Match presMatch = Regex.Match(message.Channel, presPattern);
            string presentation = message.Channel;
            if(presMatch.Success) {
                presentation = presMatch.Groups[1].Value;
            }

            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(
                new Uri(this.BaseAddress, string.Format(CultureInfo.InvariantCulture, "publish/{0}", presentation)));
            request.Method = "POST";
            request.ContentType = "text/xml";


            //TODO: This needs to be asynchornous
            request.BeginGetRequestStream(ActivePublisherClient.ContinueHttpRequest,
                new PublishHttpAsyncContext {
                    Credential = Credential,
                    Message = message,
                    Request = request,
                    Processor = ActivePublisherClient.ProcessPublishGetRequestStream,
                    OnErrorCore = onError,
                    OnSuccessCore = onSuccess
                });
        }

        public Dictionary<string, int> GetChannels(string presentation) {

            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(
                new Uri(this.BaseAddress, string.Format(CultureInfo.InvariantCulture, "channels/{0}", presentation)));
            request.Method = "POST";
            request.ContentType = "text/xml";


            Stream reqStream = request.GetRequestStream();
            //Write the request here
            XElement reqNode = new XElement(ns + "ActiveUpdate");
            //Add Authentication Info Here
            reqNode.Add(AuthorizationUtil.ToAuthenticationXML(Credential));

            string pubData = reqNode.ToString();
            byte[] stringArray = Encoding.UTF8.GetBytes(pubData);

            reqStream.Write(stringArray, 0, stringArray.Length);
            reqStream.Close();

            XElement chResponse = XElement.Load(request.GetResponse().GetResponseStream());

            if(chResponse.Name == "Error") {
                throw new Exception(string.Format("Error Retrieving Channel list: {0}", chResponse));
            }

            Dictionary<string, int> channelsToReturn = new Dictionary<string, int>();
            foreach(XElement c in chResponse.Elements(ns + "Channel")) {
                channelsToReturn[c.Attribute("ID").Value] = int.Parse(c.Attribute("Message").Value);
            }

            return channelsToReturn;

        }

        static void ContinueHttpRequest(IAsyncResult result)
        {
            HttpAsyncContext context = (HttpAsyncContext)result.AsyncState;
            try
            {
                context.Processor(result, context);
            }
            catch (Exception e)
            {
                if (context != null)
                {
                    if (context.Stream != null)
                    {
                        context.Stream.Close();
                    }
                    if (context.Response != null)
                    {
                        context.Response.Close();
                    }
                    context.Exception = e;
                    try
                    {
                        context.OnError();
                    }
                    catch (Exception)
                    {
                        // empty
                    }
                }
            }
        }

        static void ProcessPublishGetRequestStream(IAsyncResult result, HttpAsyncContext context)
        {
            PublishHttpAsyncContext publishContext = (PublishHttpAsyncContext)context;
            context.Stream = publishContext.Request.EndGetRequestStream(result);
            
            //Write the request here
            XElement pubNode = new XElement(ns + "ActiveUpdate");
            //Add Authentication Info Here
            pubNode.Add(AuthorizationUtil.ToAuthenticationXML(publishContext.Credential));
            pubNode.Add(publishContext.Message.ToXml());

            string pubData = pubNode.ToString();
            byte[] stringArray = Encoding.UTF8.GetBytes(pubData);

            context.Stream.Write(stringArray, 0, stringArray.Length);
            context.Stream.Close();

            context.Processor = ActivePublisherClient.ProcessGetResponse;
            context.Request.BeginGetResponse(ActivePublisherClient.ContinueHttpRequest, context);
        }

        static void ProcessGetResponse(IAsyncResult result, HttpAsyncContext context)
        {
            context.Response = (HttpWebResponse)context.Request.EndGetResponse(result);
            if (context.Response.StatusCode != HttpStatusCode.OK)
            {
                throw new InvalidOperationException(
                    string.Format(CultureInfo.InvariantCulture,
                    "{0}. Server returned status code {1}: {2}.",
                    context.ExceptionMessagePrefix,
                    context.Response.StatusCode,
                    context.Response.StatusDescription));
            }
            context.Stream = context.Response.GetResponseStream();
            PublishHttpAsyncContext publishContext = (PublishHttpAsyncContext)context;
            XElement body = XElement.Load(context.Stream);
           
            publishContext.Message = new ActiveMessage() { Body = body };
            //Interpret the message here
            if(body.Name == "Error") {
                context.OnError();
            }

            context.OnSuccess();
        }


        public void Unsubscribe(string channel)
        {
            if (this.MinimizeConnectionUse)
            {
                this.pollManager.RemoveSubscription(channel);
            }
            else
            {
                lock (this.syncRoot)
                {
                    List<HttpLongPollManager> pollManagerList;
                    if(this.pollManagers.TryGetValue(channel, out pollManagerList))
                    {
                        foreach (HttpLongPollManager pm in pollManagerList)
                        {
                            pm.RemoveAllSubscriptions();
                        }
                        this.pollManagers.Remove(channel);
                    }
                }
            }
        }

        public void UnsubscribeAll()
        {
            if (this.MinimizeConnectionUse)
            {
                this.pollManager.RemoveAllSubscriptions();
            }
            else
            {
                lock (this.syncRoot)
                {
                    foreach (List<HttpLongPollManager> pml in this.pollManagers.Values)
                    {
                        foreach (HttpLongPollManager pm in pml)
                        {
                            pm.RemoveAllSubscriptions();
                        }
                    }
                    this.pollManagers.Clear();
                }
            }
        }

        abstract class HttpAsyncContext
        {
            public int Offset { get; set; }
            public byte[] Buffer = new byte[256];
            public int ResultId { get; set; }
            public Exception Exception { get; set; }
            public HttpWebRequest Request { get; set; }
            public HttpWebResponse Response { get; set; }
            public Stream Stream { get; set; }
            public Action<IAsyncResult, HttpAsyncContext> Processor { get; set; }
            public abstract string ExceptionMessagePrefix { get; }

            public abstract void OnSuccess();
            public abstract void OnError();
        }

        class PublishHttpAsyncContext : HttpAsyncContext
        {
            public NetworkCredential Credential { get; set; }
            public ActiveMessage Message { get; set; }
            public Action<ActiveMessage> OnSuccessCore { get; set; }
            public Action<ActiveMessage, Exception> OnErrorCore { get; set; }
            public override string ExceptionMessagePrefix { get { return "Error while publishing a message."; } }


            public override void OnSuccess()
            {
                if (this.OnSuccessCore != null)
                {
                    this.Message.MessageId = this.ResultId;
                    this.OnSuccessCore(this.Message);
                }
            }

            public override void OnError()
            {
                if (this.OnErrorCore != null)
                {
                    this.OnErrorCore(this.Message, this.Exception);
                }
            }
        }
    }
}
